/*
 * Licensed Materials - Property of tenxcloud.com
 * (C) Copyright 2018 TenxCloud. All Rights Reserved.
 * 2018-06-13  @author lizhen
 */

package crypto

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/sha512"
	"golang.org/x/crypto/pbkdf2"
	"io"
)

func NewAEADCryptor(c config) *aead {
	return &aead{
		secret: c.Secret(),
	}
}

type aead struct {
	secret []byte
}

func (a aead) Encrypt(content []byte) (encrypted []byte, err error) {
	var salt []byte
	if salt, err = randBytes(64); err != nil {
		return
	}
	var block cipher.Block
	if block, err = aes.NewCipher(a.key(salt)); err != nil {
		return
	}
	var iv []byte
	if iv, err = randBytes(12); err != nil {
		return
	}
	if _, err = io.ReadFull(rand.Reader, iv); err != nil {
		return
	}
	var aes256gcm cipher.AEAD
	if aes256gcm, err = cipher.NewGCM(block); err != nil {
		return
	}
	withTag := aes256gcm.Seal(nil, iv, content, nil)
	encryptedLen := len(withTag) - 16
	withoutTag := withTag[:encryptedLen]
	tag := withTag[encryptedLen:]
	encrypted = bytes.Join([][]byte{salt, iv, tag, withoutTag}, []byte{})
	return
}

func (a aead) Decrypt(encrypted []byte) (content []byte, err error) {
	if len(encrypted) <= 92 {
		err = Undecryptable
		return
	}
	salt := encrypted[:64]
	iv := encrypted[64:76]
	tag := encrypted[76:92]
	withoutTag := encrypted[92:]
	withTag := bytes.Join([][]byte{withoutTag, tag}, []byte{})
	var block cipher.Block
	if block, err = aes.NewCipher(a.key(salt)); err != nil {
		return
	}
	var aes256gcm cipher.AEAD
	if aes256gcm, err = cipher.NewGCM(block); err != nil {
		return
	}
	content, err = aes256gcm.Open(nil, iv, withTag, nil)
	return
}

func (a aead) key(salt []byte) []byte {
	return pbkdf2.Key(a.secret, salt, 2145, 32, sha512.New)
}

func randBytes(length int) (bytes []byte, err error) {
	bytes = make([]byte, length)
	if _, err = io.ReadFull(rand.Reader, bytes); err != nil {
		return
	}
	return
}
